use std::mem;
use std::ptr::hash;
use argon2::{Argon2, password_hash::{
    rand_core::OsRng,
    SaltString,
}, PasswordHash, PasswordHasher, PasswordVerifier};

use crate::User;
use crate::AppError;

use super::USER_QUERY_NO_PWD_BY_EMAIL_SQL;
use super::USER_QUERY_BY_EMAIL_SQL;
use super::USER_INSERT_SQL;

impl User {
    pub async fn find_by_email(email: &str, pool: &sqlx::PgPool) -> Result<Option<User>, AppError> {
        let user = sqlx::query_as(USER_QUERY_NO_PWD_BY_EMAIL_SQL)
            .bind(email)
            .fetch_optional(pool)
            .await?;

        Ok(user)
    }

    pub async fn verify(email: &str, password: &str, pool: &sqlx::PgPool) -> Result<Option<User>, AppError> {
        let user: Option<User> = sqlx::query_as(USER_QUERY_BY_EMAIL_SQL).bind(email)
            .fetch_optional(pool).await?;

        if let Some(mut user) = user {
            let pwd_hash = mem::take(&mut user.password_hash);
            let is_verify = Self::verify_password(password, &pwd_hash.unwrap_or_default())?;
            if is_verify {
                Ok(Some(user))
            } else {
                Ok(None)
            }
        } else {
            Ok(None)
        }
    }

    fn hash_password(pwd: &str) -> Result<String, AppError> {
        let salt = SaltString::generate(&mut OsRng);
        let argon2 = Argon2::default();

        let ret = argon2.hash_password(pwd.as_bytes(), &salt)?.to_string();

        Ok(ret)
    }

    fn verify_password(pwd: &str, hash_pwd: &str) -> Result<bool, AppError> {
        let argon2 = Argon2::default();
        let password_hash = PasswordHash::new(hash_pwd)?;
        let is_verify = argon2.verify_password(pwd.as_bytes(), &password_hash).is_ok();

        Ok(is_verify)
    }

    pub async fn create(email: &str, fullname: &str, password: &str, pool: &sqlx::PgPool) -> Result<Self, AppError> {
        let user  = Self::find_by_email(email, pool).await?;
        if user.is_some() {
            return Err(AppError::EmailAlreadyExists(email.clone().to_string()))
        }

        let hash_pwd = Self::hash_password(password)?;

        let user = sqlx::query_as(USER_INSERT_SQL)
            .bind(email).bind(fullname).bind(hash_pwd)
            .fetch_one(pool).await?;

        Ok(user)
    }
}

#[cfg(test)]
mod test {
    use sqlx_db_tester::TestPg;
    use crate::User;

    use anyhow::Result;

    #[tokio::test]
    async fn user_test() -> Result<()> {
        // windows: let tdb = TestPg::new("postgres://cuixuecheng@localhost:5432".to_string()
        let tdb = TestPg::new("postgres://cuixuecheng@localhost:5432".to_string(),
        std::path::Path::new("../migrations"));

        let pool = tdb.get_pool().await;

        let email = "cui@gmail.com";
        let fullname = "cui";
        let pwd = "cui123";

        let user = User::create(email, fullname, pwd, &pool).await?;
        assert_eq!(user.fullname, fullname);
        assert_eq!(user.email, email);
        assert!(user.id > 0);

        let user = User::find_by_email(email, &pool).await?;
        assert!(user.is_some());

        let user = user.unwrap();
        assert_eq!(user.fullname, fullname);
        assert_eq!(user.email, email);

        let user = User::verify(email, pwd, &pool).await?;
        // assert!(user);

        Ok(())
    }
}